﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Text;

using Antlr.Runtime;
using Antlr.Runtime.Tree;

namespace SqlAntlr
{
    
public class SqlQuery : IView {	

    private int qid;            // query id
    private SqlQuery parent;    // parent in scope
    private String alias;
    
    private IToken surroundingToken = null; //this is for subqueries within predicates only
    
    // maintaining structure of the queries

    // union, intersects, excepts
    private List<SqlQuery> setQueries = new List<SqlQuery>();
    // appears in the from clause
    private List<SqlQuery> tableQueries = new List<SqlQuery>();
    // appears in other clauses
    private List<SqlQuery> subQueries = new List<SqlQuery>();

    private List<SqlQuery.Table> tables = new List<SqlQuery.Table>();

    // parse tree information
    private CommonTree statement;   // entire query

    private CommonTree selectClause;
    private CommonTree fromClause;
    private CommonTree whereClause;
    private CommonTree groupByClause;
    private CommonTree havingClause;
    private CommonTree orderByClause;

    /////////////////////////////////////////////////////////////////////////
    // Semantic information

    private Dictionary<String,Column> colMap = new Dictionary<String,Column>();
    private IList<Column>    columns = new List<Column>(); // selected
    private IList<Order>     order;  // ordering information
    private IList<Column>    groups;
    
    private IList<CommonTree> joinPred = new List<CommonTree>();

    public SqlQuery() {}

    private SqlQuery(SqlQuery o) {
        selectClause = o.selectClause;
        fromClause = o.fromClause;        
        whereClause = o.whereClause;
        groupByClause = o.groupByClause;
        havingClause = o.havingClause;
        orderByClause = o.orderByClause;
    }

    //these methods are just for subqueries in predicates
    public void setSurroundingToken(IToken surroundingToken){ this.surroundingToken = surroundingToken; }
    public String getSurroundingToken(){ return surroundingToken.Text;}
    
    // membership
    public int getID() { return qid; }
    public bool isRoot() { return parent == null; }
    //public SqlQuery getParent() { return parent; }
    public void setParent(SqlQuery p) { parent = p; }

    // collection method

    public int Count {
        get { return setQueries.Count; }
    }

    public void add(SqlQuery q) {
        q.setParent(this);
        setQueries.Add(q);
        // FIXME: leave the assignment as it is?
    }

    public List<SqlQuery> getQueries() {
        return setQueries;
    }

    // for subqueries in FROM clause

    public void addSubQueryAtFrom(SqlQuery q) {
        q.setParent(this);
        tableQueries.Add(q);
    }

    public List<SqlQuery> getSubQueriesAtFrom() {
        return tableQueries;
    }

    /**
     * get all tables or subqueries appeared in FROM clause of
     * this query object
     */
    public List<IView> getTableSources() {
        List<IView> r = new List<IView>( tables.Count + tableQueries.Count );        
        r.AddRange(tables);
        r.AddRange(tableQueries);
        return r;
    }


    // for subqueries
    public void addSubQuery(SqlQuery q) {
        q.setParent(this);
        subQueries.Add(q);
    }

    //for subqueries within a predicate
    public void addSubQueryInPredicate(SqlQuery q, IToken surroundingToken){
    	addSubQuery(q);
    	q.setSurroundingToken(surroundingToken); 
    }
    
    public List<SqlQuery> getSubQueries() { return subQueries; }

    public bool hasSubQueries() { return subQueries.Count > 0; }

    // getter/setter

    /* 
     * below are only set by SqlParser. thus package-wise accessible.
     */
    internal void setStatement(CommonTree t) { statement = t; }
    internal void setSelect(CommonTree t) { selectClause = t; }
    internal void setFrom(CommonTree t) { fromClause = t; }
    internal void setWhere(CommonTree t) { whereClause = t; }
    internal void setGroupBy(CommonTree t) { groupByClause = t; }
    internal void setHaving(CommonTree t) { havingClause = t; }
    internal void setOrderBy(CommonTree t) { orderByClause = t; }

    public CommonTree getStatement() { return statement; }
    public CommonTree getSelect() { return selectClause; }
    public CommonTree getFrom() { return fromClause; }
    public CommonTree getWhere() { return whereClause; }
    public CommonTree getGroupBy() { return groupByClause; }
    public CommonTree getHaving() { return havingClause; }
    public CommonTree getOrderBy() { return orderByClause; }

    public bool hasWhere() { return whereClause != null; }
    public bool hasHaving() { return havingClause != null; }

    public bool hasOrderBy() { return order != null; }
    public IList<Order> getOrdering() { return order; }

    public bool hasGroupBy() { return groups != null; }
    public IList<Column> getGroups() { return groups; }

    private void reassignParent(List<SqlQuery> views) {
        foreach ( SqlQuery q in views ) {
            q.setParent(this);
        }
    }

    /**
     * if current statement only contains a single SFWHG,
     * unnesting parent SqlQuery created for potential wrapping
     */

    internal void wrap()
    {
        if ( setQueries.Count == 1 ) {
            // unnesting the queries
            SqlQuery o = setQueries[0];

            //assert( statement == o.statement );
            Debug.Assert( o.orderByClause == null );
            Debug.Assert( tableQueries.Count == 0 );
            Debug.Assert( subQueries.Count == 0 );

            qid = o.qid;
            alias = o.alias;

            //logger.log(Level.FINE,"Wrapping \"{0}\"",statement.toStringTree());
            //logger.log(Level.FINE,"Wraped \"{0}\"",o.statement.toStringTree());

            //statement = o.statement;
            selectClause = o.selectClause;
            fromClause = o.fromClause;
            whereClause = o.whereClause;
            groupByClause = o.groupByClause;
            havingClause = o.havingClause;

            setQueries = o.setQueries;
            tableQueries = o.tableQueries;
            subQueries = o.subQueries;
            tables = o.tables;

            reassignParent(setQueries);
            reassignParent(tableQueries);
            reassignParent(subQueries);

            foreach ( SqlQuery.Table t in tables ) {
                t.setParent(this);
            }

            o.clear();
        }
    }

    private void clear() {
        alias = null;

        statement = null;
        selectClause = null;
        fromClause = null;
        whereClause = null;
        groupByClause = null;
        havingClause = null;
        orderByClause = null;

        setQueries = null;
        tableQueries = null;
        subQueries = null;
        tables = null;
        parent = ( parent == null ) ? null : parent.parent;
    }


    // Utilities -- print out string format

    protected String toString(ITokenStream s,CommonTree t) {
        return ( t == null ) ? null : s.ToString( t.TokenStartIndex, t.TokenStopIndex );
    }

    protected void toString(ITokenStream s,StringBuilder buf,int d) {
        string space = "";
        for (int i = 0; i < d; i++)
        {
            space += " ";
        }
        
        String str = toString(s,selectClause);
        if ( str != null ) {
            buf.Append(space); buf.Append(str); buf.Append('\n');
        }
        str = toString(s,fromClause);
        if ( str != null ) {
            buf.Append(space); buf.Append(str); buf.Append('\n');
        }
        str = toString(s,whereClause);
        if ( str != null ) {
            buf.Append(space); buf.Append(str); buf.Append('\n');
        }
        str = toString(s,groupByClause);
        if ( str != null ) {
            buf.Append(space); buf.Append(str); buf.Append('\n');
        }
        str = toString(s,havingClause);
        if ( str != null ) {
            buf.Append(space); buf.Append(str); buf.Append('\n');
        }
        str = toString(s,orderByClause);
        if ( str != null ) {
            buf.Append(space); buf.Append(str); buf.Append('\n');
        }

        if ( setQueries.Count != 0 ) {
            buf.Append(space); buf.Append("SET QUERIES:\n");
            foreach ( SqlQuery q in setQueries ) {
                q.toString(s,buf,d+1);
            }
        }
        if ( tableQueries.Count != 0 ) {
            buf.Append(space); buf.Append("TABLE QUERIES:\n");
            foreach ( SqlQuery q in tableQueries ) {
                q.toString(s,buf,d+1);
            }
        }
        if ( subQueries.Count != 0 ) {
            buf.Append(space); buf.Append("SUB QUERIES:\n");
            foreach ( SqlQuery q in subQueries ) {
                q.toString(s,buf,d+1);
            }
        }
    }

    public String toString(ITokenStream s) {
        StringBuilder buf = new StringBuilder();
        toString(s,buf,0);
        return buf.ToString();
    }

    //////////////////////////////////////////////////////////////////////////

    //////////////////////////////////////////////////////////////////////////

    public class Table : IView {
        CommonTree expr;
        String name;
        String alias;

        IView parent; // containing this table

        Dictionary<String,Column> cmap = new Dictionary<String,Column>();
        IList<Column> columns;

        public Table(IView p, RelationSchema s) {
            name = alias = s.getRelationName();
            parent = p;
            columns = new List<Column>(s.size());
            foreach ( String c in s.getAll() ) {
                Column.Value col = new Column.Value(this,c);
                columns.Add(col);                
                cmap[c] = col;
            }
        }

        public int getCardinality() {
            return columns.Count;
        }

        public IList<Column> getColumns(bool selected)
        {
            return columns;
        }

        public Column getFirstColumn() {
            return columns[0];
        }

        public Column getColumn(String n,bool selected) {
            return cmap[n];
        }

        public String getAlias() { return alias; }
        public String getRealName() { return name; }
        public void setAlias(String s) { alias = s; }
        void setParent(IView v) { parent = v; }
        public IView getParent() { return parent; }
        public IView getView(String s) { return null; }

        public bool isSame(IView v) {
            if ( this == v ) return true;
            if ( ! (v is Table) ) return false;
            Table o = (Table)v;
            return name.Equals(o.name);
        }

        public override String ToString() {
            return SqlQuery.ToString(this,0);
        }

        public String toObjectString() {
            return base.ToString();
        }

        internal void setParent(SqlQuery sqlQuery)
        {
            throw new NotImplementedException();
        }
    };


    //////////////////////////////////////////////////////////////////////////
    // view interface implementation

    private Dictionary<String,IView> viewMap = new Dictionary<String,IView>();

    public int getCardinality() { return columns.Count; }

    /**
     * @param selected if <code>true</code>, return only selected columns.
     */
    public IList<Column> getColumns(bool selected)
    {
        if ( selected ) {
            return columns;
        }
        
        List<Column> cols = new List<Column>();
        foreach ( SqlQuery q in tableQueries ) {
            cols.AddRange( q.getColumns(true) );
        }
        foreach ( Table t in tables ) {
            cols.AddRange( t.getColumns(true) );
        }
        return cols;
    }

    public Column getFirstColumn() {
        return columns[0];
    }

    public Column getColumn(String n,bool selected) {
        // first check selected columns
        Column c = colMap[n];
        if ( c == null && !selected ) {
            // the selected flag will be always true at this moment
            foreach ( SqlQuery q in tableQueries ) {
                c = q.getColumn(n,true);
                if ( c != null ) return c;
            }
            foreach ( Table t in tables ) {
                c = t.getColumn(n,true);
                if ( c != null ) return c;
            }
        }

        return c;
    }

    public String getAlias() { return alias; }
    public String getRealName() { return alias; }

    public void setAlias(String s) { alias = s.ToLower(); }

    public IView   getParent() { return parent; }

    /**
     * @param s name of view
     * @return the object named as <code>s</code> or <code>null</code>
     */
    public IView   getView(String s) {
        // does x is defined in this view?
        return viewMap[s];
    }

    // SCOPE operation
    internal SqlQuery getSubQueryAtFrom(CommonTree n)
    {
        //System.err.println("Query hashcode = "+n.hashCode()+";"+n);

        foreach ( SqlQuery q in tableQueries ) {
            //System.err.println("Comparing hashcode = "+q.statement.hashCode()+";"+q.statement);
            if ( q.statement == n ) {
                return q;
            }
        }
        return null;
    }
    internal SqlQuery getSubQuery(CommonTree n) {
        foreach ( SqlQuery q in subQueries ) {
            if ( q.statement == n )
                return q;
        }
        return null;
    }


    public class Resolver {
        List<IView> stack = new List<IView>();

        public bool Empty { get {return stack.Count == 0; }}
        public void push(IView v) { stack.Add(v); }
        public IView peek() { return stack[stack.Count-1]; }
        public void pop() { stack.RemoveAt(stack.Count-1); }
        public void clear() { stack.Clear(); }

        public IView   resolve(String r) {
            IView v = null;

            for ( int idx = stack.Count-1; idx >= 0; --idx ) {
                IView q = stack[idx];
                v = q.getView(r);
                if ( v != null ) break;
            }

            return v;
        }

        public Column resolve(String r,String n) {
            Column c = null;

    //        logger.log(Level.FINE,"Begin resolving {0}.{1}",new Object[] {r,n});

            // Being in the stack means all columns are visible to the top
            // of the stack including SELECT aliases.

            for ( int idx = stack.Count-1; idx >= 0; --idx ) {
                IView q = stack[idx];

  //              logger.log(Level.FINE,"  Checking {0}",q);

                if ( r == null || r.Length == 0 ) {
                    // there must be a unique column which n refers to
                    c = q.getColumn(n,false);
                } else {
                    // check whether the view exists
                    IView v = q.getView(r);
                    if ( v != null ) {
                        // check whether the column is defined in the view
                        c = v.getColumn(n,false);
                    }
                }

                if ( c != null )
                    break;
            }

            return c;
        }
    }

    public int resolve(SqlResolve parser)  {
        Resolver resolver = new Resolver();
        parser.setResolver(resolver);
        parser.Clear();
        resolve(parser,resolver);
        return parser.getTotalErrors();
    }

    private void resolve(SqlResolve parser,Resolver resolver) {
        if ( setQueries.Count == 0 ) {
            Debug.Assert( selectClause != null && fromClause != null );

            // process all subqueries and tables in FROM clause
            // we don't push this object since they are not available
            // to the subqueries at the same level

            // first, add all concrete tables
            prepare(parser,fromClause);
            parser.fromClause();

            // move on to all subqueries appeared in FROM clause
            foreach ( SqlQuery q in tableQueries ) {
                q.resolve(parser,resolver);
                //System.err.println("--- table subquery ---");
                //System.err.println(q);
            }

            // we're done. ready to resolve other clauses
            // make this view available
            resolver.push(this);

            // resolve join predicates
            foreach ( CommonTree p in joinPred ) {
                prepare(parser,p);
                parser.resolve_predicate();
            }

            // resolve all subqueries
            foreach ( SqlQuery q in subQueries ) {
                q.resolve(parser,resolver);
                //System.err.println("--- subquery ---");
                //System.err.println(q);
            }

            if ( whereClause != null ) {
                prepare(parser,whereClause);
                parser.whereClause();
            }

            // finally handle select clause
            prepare(parser,selectClause);
            parser.selectClause();

            if ( groupByClause != null ) {
                prepare(parser,groupByClause);
                groups = parser.groupByClause();
            }

            if ( havingClause != null ) {
                prepare(parser,havingClause);
                parser.havingClause();
            }

            // order by is the last thing to resolve
            if ( orderByClause != null ) {
                prepare(parser,orderByClause);
                order = parser.orderByClause();
            }

            // all done. remove this view and return to parent
            resolver.pop();
        } else {
            Debug.Assert( selectClause == null && fromClause == null );

            foreach ( SqlQuery q in setQueries ) {
                q.resolve(parser,resolver);
                //System.err.println(q);
            }

            // COPY SELECT CLAUSE FROM SET QUERY
            // FIXME: should we do this for all set queries?
            addColumns(setQueries[0]);

            if ( orderByClause != null ) {
                // resolve
                resolver.push(setQueries[0]);

                prepare(parser,orderByClause);
                order = parser.orderByClause();

                resolver.pop();
            }
        }
    }

    internal void add(SqlQuery.Table v) {
        viewMap[v.getAlias()] = v;
        viewMap[v.getRealName()] = v;
        tables.Add(v);
    }

    internal void addAlias(SqlQuery v)
    {
        // already added
        viewMap[v.getAlias()] = v;
    }

    internal void add(Column c)
    {
        columns.Add(c);

        String n = c.getRealName();
        String a = c.getAlias();

        if ( n != null ) colMap[n] = c;
        if ( a != null ) colMap[a] = c;
    }

    internal void addColumns(IView v)
    {
        foreach ( Column c in v.getColumns(true) ) {
            add(new Column.Value(this,c));
        }
    }

    internal void star()
    {
        foreach ( IView v in tables )
            addColumns(v);
        foreach ( IView v in tableQueries )
            addColumns(v);
    }

    /**
     * add join predicate if there is any
     */
    internal void addJoinPredicate(CommonTree t) { joinPred.Add(t); }

    public bool hasJoinPredicates() { return  joinPred.Count != 0; }
    public IList<CommonTree> getJoinPredicates() { return joinPred; }

    /**
     * get number of joins for this object.
     * @return number of joins required for this select statement
     */
    public int getNumberOfJoins() {
        return tableQueries.Count + tables.Count - 1;
    }

    /**
     * get all IViews in FROM clause
     */
    public ISet<IView> getSourceViews() {
        ISet<IView> r = new HashSet<IView>();
        r.UnionWith(tableQueries);
        r.UnionWith(tables);
        return r;
    }

    private void prepare(SqlResolve parser,CommonTree t) {
        CommonTreeNodeStream nodeStream = new CommonTreeNodeStream(t);
        nodeStream.TokenStream = parser.getTokenStream();
        parser.Reset();
        parser.setQuery(this);
        parser.TreeNodeStream = nodeStream;
    }

    static String ToString(IView v,int d) {
        StringBuilder buf = new StringBuilder();

        String n = v.getRealName();
        String a = v.getAlias();

        if ( n == null || n.Length == 0 ) {
            buf.Append("<anon>");
        } else {
            buf.Append(n);
        }

        buf.Append('(');
        foreach ( Column c in v.getColumns(true) ) {
            buf.Append(c);
            buf.Append(',');

        }
        if ( buf[buf.Length-1] == ',' )
            buf.Length = buf.Length -1;
//            buf.setLength(buf.length()-1);

        buf.Append(')');
        
        if ( a != null && (n == null || !n.Equals(a) ) ) {
            buf.Append(" AS ");
            buf.Append(a);
        }

        buf.Append(v.toObjectString());

        return buf.ToString();
    }

    public String toObjectString() {
        return base.ToString();
    }

    public override String ToString() {
        return ToString(this,0);
    }

    public enum ApplyOrder
    {
        PRE_ORDER,
        POST_ORDER
    }
        

    /**
     * use this to traverse SqlQuery object hierarchy
     */
    public interface IApply {
        
        /**
         * get ordering information
         */
        ApplyOrder getOrder();

        /**
         * applying designated operation. this will be applied
         * to the given query only once and in specified order
         * @param query visiting query
         * @return <code>true</code> if early termination is required. otherwise, <code>false</code>.
         */
        bool apply(SqlQuery query);

        /**
         * called as soon as the traverse visit this query object 
         * in the hierarchy. this will be called before apply in any
         * visiting order
         */
        void visit(SqlQuery query);

        /**
         * called when the traverse about to leave this query object 
         * in the hierarchy. this will be called after apply in any
         * visiting order
         */
        void leave(SqlQuery query);
    }

    private bool apply2(IApply func) {
        func.visit(this);

        if ( func.getOrder() == ApplyOrder.PRE_ORDER ) {
            if ( func.apply(this) ) return true;
        }

        // do recursion
        foreach ( SqlQuery q in setQueries )   {
            if ( q.apply2(func) ) return true;
        }
        foreach ( SqlQuery q in tableQueries ) {
            if ( q.apply2(func) ) return true;
        }
        foreach ( SqlQuery q in subQueries ) {
            if ( q.apply2(func) ) return true;
        }

        if ( func.getOrder() == ApplyOrder.POST_ORDER ) {
            if ( func.apply(this) ) return true;
        }

        func.leave(this);

        return false;
    }

    public void apply(IApply func) {
        apply2(func);
    }

    public bool isSame(IView v) {
        if ( this == v ) return true;
        if ( ! (v is SqlQuery) ) return false;
        SqlQuery o = (SqlQuery) v;

        // FIXME: implement!!

        return false;
    }
    
    //----------
    
    
}
}
